package cn.tworice.system.service.login;

import cn.tworice.auth.config.AuthProperties;
import cn.tworice.auth.service.AuthManager;
import cn.tworice.common.util.*;
import cn.tworice.common.util.cryption.MD5Utils;
import cn.tworice.system.controller.login.vo.RegVO;
import cn.tworice.system.service.resources.CacheConst;
import cn.tworice.auth.service.VerifyManger;
import cn.tworice.auth.util.TokenUtils;
import cn.tworice.captcha.core.CaptchaBase64;
import cn.tworice.system.dao.resource.po.ResourcesDto;
import cn.tworice.common.framework.mail.core.MailExecutor;
import cn.tworice.log.dao.po.LoginLogDO;
import cn.tworice.common.vo.RequestResult;
import cn.tworice.ip.util.IpAddrUtils;
import cn.tworice.ip.util.UAUtil;
import cn.tworice.log.service.LogService;
import cn.tworice.system.conifg.LoginProperties;
import cn.tworice.system.controller.login.constand.LoginStateConst;
import cn.tworice.system.controller.login.constand.SuperAdminConst;
import cn.tworice.system.controller.login.vo.LoginVO;
import cn.tworice.system.dao.user.po.UserDO;
import cn.tworice.system.dao.role.po.RoleDO;
import cn.tworice.system.controller.resources.vo.ResourceResult;
import cn.tworice.system.exception.LoginException;
import cn.tworice.system.service.resources.ResourcesService;
import cn.tworice.system.service.role.RoleService;
import cn.tworice.system.service.user.UserService;
import cn.tworice.system.service.user.UserStatusConst;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.*;

@Service
@Slf4j
public class LoginServiceImpl implements LoginService {

    @Resource
    private VerifyManger verifyManger;

    @Resource
    private AuthManager authManager;

    @Resource
    private LogService logService;

    /* 登录验证码 */
    @Resource
    private AgingMap<String,Object> agingMap;

    /* 邮件工具 */
    @Resource
    private MailExecutor mailUtil;

    @Resource
    private UserService userService;

    @Resource
    private RoleService roleService;

    @Resource
    private ResourcesService resourcesService;
    
    /**
     * 登录配置参数
     */
    @Resource
    private LoginProperties loginProperties;

    /**
     * 存储二次验证Key
     */
    private AgingMap<String,Object> reAuthAgingMap;

    @Resource
    private AuthProperties authProperties;

    @Resource
    private MailExecutor mailExecutor;

    @Override
    public CaptchaBase64 getLoginCaptcha(HttpServletRequest request, String code) {
        String clientIP = NetworkUtil.getClientIP(request);
        if (code != null) {
            // 删除原验证码
            agingMap.remove(code);
        }
        CaptchaBase64 captcha = new CaptchaBase64(loginProperties.getCaptchaType());
        // 生成UUID作为Key 将验证码的结果存在AgingMap
        captcha.setKey(StringUtils.generateUuid());
        if (agingMap.put(captcha.getKey(), captcha.getResult().toLowerCase(), loginProperties.getCaptchaAging()) == null) {
            if (loginProperties.getExceeded() && authProperties.getMailBlast() != null) {
                mailExecutor.sendMail(authProperties.getMailBlast(), "检测到恶意登录请求", "IP地址：" + clientIP + "，检测到客户端频繁请求登录验证码");
            }
            throw new LoginException("验证码请求频繁，请稍后再试");
        }
        return captcha;
    }

    /**
     * 获取邮箱注册验证码
     * @param email 邮箱
     * @return 验证码key
     */
    @Override
    public String getRegCaptcha(String email) {
        // 生成四位数验证码
        String key = StringUtils.generateUuid();
        String verCode = mailUtil.sendEmailCaptcha(4, email,"注册");
        if (agingMap.put(key, verCode, loginProperties.getCaptchaAging()) == null) {
            throw new LoginException("验证码请求频繁，五分钟后再试");
        }
        // 构建返回客户端数据对象
        log.debug("验证码已发送。key-{}，code-{}", key, verCode);
        return key;
    }

    @Override
    public String getCaptcha(String email) {
        // 判断系统中是否存在该邮箱
        UserDO userDO = userService.existUserByEmail(email);
        if (userDO == null) {
            throw new LoginException("邮箱不存在");
        }
        // 生成四位数验证码
        String verCode = mailUtil.sendEmailCaptcha(4, email,"验证码");
        if (agingMap.put(email, verCode, loginProperties.getCaptchaAging()) == null) {
            throw new LoginException("验证码请求频繁，五分钟后再试");
        }
        // 构建返回客户端数据对象
        log.debug("验证码已发送。email-{}，code-{}", email, verCode);
        return email;
    }

    @Override
    public RequestResult checkMailCaptchaLogin(LoginVO loginVO, HttpServletRequest request) {
        if (!this.agingMap.exist(loginVO.getLoginName(), loginVO.getCaptcha())) {
            throw new LoginException("验证码错误");
        }
        this.agingMap.remove(loginVO.getLoginName());
        UserDO userDO = userService.existUserByEmail(loginVO.getLoginName());
        return this.loginSuccess(userDO, getRolesById(userDO.getId()));
    }

    /**
     * 处理登录请求
     * @param loginVO 登录参数
     * @param request 登录请求
     * @return 登录结果
     */
    @Override
    public RequestResult checkLogin(LoginVO loginVO, HttpServletRequest request) {
        // 判断登录验证码
        this.verifyCaptcha(loginVO);
        
        // 判断是否为超级管理员
        if(isSuperAdmin(loginVO)) return this.returnSuperAdminResult();
        
        // 判断账号密码是否正确
        UserDO user = this.verifyLogin(loginVO,request);
        if (Objects.isNull(user)) {
            throw new LoginException("用户名或密码有误");
        }

        // 判断账号是否被停用
        if (UserStatusConst.BAN.equals(user.getStatus())) {
            this.saveLog(loginVO, LoginStateConst.LOGIN_FAIL, request);
            throw new LoginException("账户已被停用");
        } else if(loginProperties.getReAuth()
                && !StringUtils.isEmpty(user.getEmail()) && RegularTable.EMAIL.verify(user.getEmail())){ // 重复验证
            this.saveLog(loginVO, LoginStateConst.LOGIN_SUCCESS, request);
            /*
              返回一个标识和一个临时key表示需要二次验证，并且直接向邮箱发送验证码
              前端收到后直接展示二次验证界面，并倒计时重新发送
              前端输入验证码后同时携带key到后端服务器验证
              返回登录结果
             */
            return this.buildReAuth(user);
        }else {
            this.saveLog(loginVO, LoginStateConst.LOGIN_SUCCESS, request);
            // 构建返回客户端内容
            return this.loginSuccess(user, getRolesById(user.getId()));
        }
    }



    /**
     * 二次验证登录
     * @param key Key
     * @param varCode 用户的输入
     * @return 登录结果
     */
    @Override
    public RequestResult checkLogin(String key, String varCode,HttpServletRequest request) {
        if (!this.reAuthAgingMap.exist(key, varCode)) {
            throw new LoginException("验证码错误");
        }
        this.reAuthAgingMap.remove(key);

        // 构建返回客户端内容
        return this.loginSuccess(userService.getInfo(key), getRolesById(key));
    }

    /**
     * 注册账号
     * @param regVO 注册参数
     * @return 注册结果
     */
    @Override
    public String checkReg(RegVO regVO) {
        // 校验注册角色是否合法
        Integer roleId = this.verifyRegRoleId(regVO.getRoleId());

        // 判断验证码是否正确
        this.verifyRegCaptcha(regVO.getKey(), regVO.getCaptcha());

        // 判断登录账号是否被注册
        UserDO userDO = this.verifyReg(regVO);
        if(userDO == null){
            // 保存账号信息
            if (setUserAndRole(this.getUserDOByRegVO(regVO), roleId)) {
                return "注册成功";
            } else {
                throw new LoginException("注册异常，请重新尝试");
            }
        }else{
            // 更新密码
            userService.updatePwd(userDO.getId(), regVO.getPassWord());
            return "账号密码已更新";
        }
    }

    @Override
    public RequestResult loginSuccess(UserDO user) {
        return this.loginSuccess(user, getRolesById(user.getId()));
    }

    /**
     * 通过账号密码查询用户
     */
    @Override
    public UserDO existUser(LoginVO loginVO) {
        return userService.getUserByEmailAndPwd(loginVO.getLoginName(), loginVO.getPassword());
    }

    /**
     * 根据角色ID获取资源列表
     */
    @Override
    public List<ResourcesDto> getResourceByRole(String userId) {
        List<ResourcesDto> resources;
        // 先查看缓存
        resources = CacheConst.RESOURCES.get(userId);
        if (resources != null) {
            return resources;
        }

        // 通过管理员ID获取角色ID
        List<Integer> roles = CacheConst.ROLES.get(userId);
        if(roles==null || roles.isEmpty()){
            roles = roleService.getRoleIdsByUid(userId);
            CacheConst.ROLES.put(userId, roles);
        }
        if(roles==null || roles.isEmpty()){
            // 判断当前账户是否没有任何权限
            return null;
        }else {
            // TODO 可能写错了 根据角色ID获取拥有的资源列表(叶子节点)
            List<ResourcesDto> resourcesDtosA = resourcesService.selectResourcesByRoles(roles);
            // 遍历每一个叶子节点
            for(ResourcesDto resourcesDto:resourcesDtosA){
                if(resources ==null) resources =new ArrayList<>();
                ResourceResult result=inResources(resources,resourcesDto);
                // 标志位 true表示已挂载
                if(result.getStatus()) {
                    resources = result.getResult();
                }else {
                    // 未挂载则需要寻找该节点的根节点
                    resources.add(getParent(resourcesDto));
                }
            }

        }
        CacheConst.RESOURCES.put(userId, resources);

        return resources;
    }



    /**
     * 获取系统所有资源
     */
    @Override
    public List<ResourcesDto> getResourcesAll(String userId) {
        List<ResourcesDto> resourcesDtos = CacheConst.RESOURCES.get(userId);
        if (resourcesDtos != null) {
            return resourcesDtos;
        }
        resourcesDtos = resourcesService.queryResourceAll();
        // 查找子级菜单
        for (ResourcesDto dto : resourcesDtos) {
            // 查询功能列表
            List<ResourcesDto> resourcesDtos1 = getResourceByPid(dto.getId());
            for (ResourcesDto dto1 : resourcesDtos1) {
                dto1.setChildren(getResourceByPid(dto1.getId()));
            }

            // 保存页面到模块列表
            dto.setChildren(resourcesDtos1);
        }
        CacheConst.RESOURCES.put(userId, resourcesDtos);
        return resourcesDtos;
    }

    /**
     * 获取拥有的权限
     * @param id 用户标识
     */
    @Override
    public List<RoleDO> getRolesById(String id) {
        return roleService.getRoleListByUid(id);
    }

    @Override
    public UserDO getUserByLoginName(String loginName) {
        return userService.getUser(loginName);
    }

    @Override
    public boolean setUserAndRole(UserDO user, int roleId) {
        if(StringUtils.isBlank(userService.updateOrInsert(user))){
            return roleService.addUserToRole(new String[]{user.getId()}, roleId);
        }
        return false;
    }

    /**
     * 将用户登录成功后的资源列表缓存起来
     * @param userId 用户ID
     * @param list 资源列表
     **/
    @Override
    public void saveAdminResources(String userId, List<ResourcesDto> list) {
        if (list == null || list.isEmpty() || userId.isEmpty()) {
            verifyManger.addAdminResources(userId, new String[0]);
        }else {
            List<String> urls = new ArrayList<>();
            layer(list, urls);
            verifyManger.addAdminResources(userId, urls.toArray(new String[30]));
        }
    }

    @Override
    public void sendReAuthCaptcha(String key) {
        String randomNumber = MathUtils.getRandomNumber(6);
        if (this.reAuthAgingMap.put(key,randomNumber,loginProperties.getCaptchaAging()) == null) {
            throw new LoginException("验证码请求频繁，五分钟后再试");
        }
        // 发送邮件
        try {
            mailUtil.sendMail(userService.getInfo(key).getEmail(),"邮箱验证","您的验证码:" + randomNumber);
        } catch (Exception e) {
            this.reAuthAgingMap.remove(key);
            throw e;
        }
    }

    /**
     * 通过PID获取资源列表
     */
    public List<ResourcesDto> getResourceByPid(Integer pid){
        return resourcesService.selectResourcesByPid(pid);
//        return loginDao.queryResourceByPid(pid);
    }

    /**
     * 验证验证码是否正确
     * @param loginVO 用户登录信息
     */
    public void verifyCaptcha(LoginVO loginVO) {
        // 判断验证码
        if (loginVO.getCaptcha()==null || !agingMap.exist(loginVO.getKey(),loginVO.getCaptcha().toLowerCase())) {
            throw new LoginException("验证码错误或已超时");
        }
        // Key已经被使用，不再有效
        agingMap.remove(loginVO.getKey());
    }

    /**
     * TODO 递归获取资源，待优化
     * 判断resourcesA节点的父节点是否存在于resources中
     * 如存在 挂载到该节点下
     * 如不存在 查询该节点的根节点，并将该根节点存放于列表中
     * mySwitch 为标志位，true表示节点已挂载，false表示未挂载
     */
    private ResourceResult inResources(List<ResourcesDto> resources, ResourcesDto resourcesA){
        boolean mySwitch = false;
        // 遍历resources 查看 resourcesA是否存在于其中
        for(ResourcesDto resourcesDto:resources){
            // 如果resourcesDto是resourcesA的父节点 并返回
            if(resourcesDto.getId().equals(resourcesA.getPid())){
                // 将该节点挂载到父节点上 并返回
                List<ResourcesDto> list=resourcesDto.getChildren();
                if(list==null){
                    list = new ArrayList<>();
                }
                list.add(resourcesA);
                resourcesDto.setChildren(list);

                ResourceResult resourceResult = new ResourceResult();
                resourceResult.setStatus(true);
                resourceResult.setResult(resources);
                return resourceResult;
            }
            // 如果该对象的子列表不为空，则继续迭代查找
            if(resourcesDto.getChildren()!=null && !resourcesDto.getChildren().isEmpty()){
                ResourceResult map = inResources(resourcesDto.getChildren(), resourcesA);
                if(map.getStatus()){
                    mySwitch=true;
                }
            }
        }

        // 程序走到这里说明 列表中不含有
        ResourceResult resourceResult = new ResourceResult();
        resourceResult.setStatus(mySwitch);
        resourceResult.setResult(resources);
        return resourceResult;
    }

    /**
     * 通过子节点查询根节点
     * 传入子节点，返回根节点
     */
    private ResourcesDto getParent(ResourcesDto resourcesDto){
        // 判断传入对象是否为根节点
        if(resourcesDto.getPid()==0){
            return resourcesDto;
        }else {
            // 获取父节点
            ResourcesDto resourcesDtoA = resourcesService.selectResourcesDtoById(resourcesDto.getPid());
//            ResourcesDto resourcesDtoA = loginDao.queryResourcesById(resourcesDto.getPid());
            // 将子节点挂载到父节点上
            List<ResourcesDto> list = new ArrayList<>();
            list.add(resourcesDto);
            resourcesDtoA.setChildren(list);
            resourcesDto=resourcesDtoA;
            return getParent(resourcesDto);
        }
    }

    /**
     * 递归获取资源列表url
     * @param list 资源列表
     * @param urls url列表
     **/
    private synchronized void layer(List<ResourcesDto> list, List<String> urls) {
        // 跳出递归
        if (list==null||list.isEmpty()) {
            return;
        }
        // 开始递归
        for (ResourcesDto resourcesDto : list) {
            if (StringUtils.isEmpty(resourcesDto.getUrl())) {
                urls.add(resourcesDto.getUrl());
            }
            layer(resourcesDto.getChildren(),urls);
        }
    }
    
    /**
     * 构建超级管理员账户
     * @return cn.tworice.system.dao.admin.Admin
     **/
    private UserDO buildSuperAdmin() {
        UserDO user = new UserDO();
        user.setId(SuperAdminConst.Super_Admin_ID);
        user.setNickName(SuperAdminConst.Super_Nick_Name);
        return user;
    }

    /**
     * 构建超级管理员角色
     * @return cn.tworice.system.dao.role.po.Role
     **/
    private RoleDO buildSuperRole() {
        RoleDO role = new RoleDO();
        role.setId(SuperAdminConst.Super_Role_ID);
        role.setRoleName(SuperAdminConst.Super_Role_Name);
        return role;
    }

    /**
     * 构建登录成功返回结果
     * @return cn.tworice.common.vo.RequestResult 构建返回结果
     **/
    public RequestResult loginSuccess(UserDO user, List<RoleDO> roles) {
        if(user ==null){
            return RequestResult.error(400, "登录异常");
        }
        // 登录成功生成Token
        String token = TokenUtils.getAdminToken(user.getId(), this.loginProperties.getTokenTimeout());
        // 获取权限资源
        List<ResourcesDto> resources =
                SuperAdminConst.Super_Admin_ID.equals(user.getId()) ?
                        getResourcesAll(user.getId())
                        : getResourceByRole(user.getId());

        if (resources == null) {
            throw new LoginException("当前账号无任何权限");
        }

        saveAdminResources(user.getId(), resources);

        // 记录在线用户
        verifyManger.online(user.getId(),token);
        return RequestResult.success("登录成功")
                .appendData("token", token)
                .appendData("admin", user)
                .appendData("resources", resources)
                .appendData("roles", roles);
    }


    /**
     * 记录登录日志
     **/
    private void saveLog(LoginVO loginVO, Integer state,HttpServletRequest request) {
        logService.addLoginLog(new LoginLogDO()
                .setLoginName(loginVO.getLoginName())
                .setPassWord(MD5Utils.getMd5Plus(loginVO.getPassword()))
                .setStatus(state)
                .setIpAddr(IpAddrUtils.getIpAddr(request))
                .setActive(UAUtil.getDevice(request)));
    }



    /**
     * 判断当前登录账户是否超级管理员
     * @param loginVO 用户输入
     */
    private boolean isSuperAdmin(LoginVO loginVO) {
        return (loginVO.getLoginName() + "-" + loginVO.getPassword()).equals(loginProperties.getRoot());
    }

    /**
     * 构建超级管理员账户
     */
    private RequestResult returnSuperAdminResult() {
        UserDO user = this.buildSuperAdmin();
        List<RoleDO> roles = new ArrayList<>();
        roles.add(this.buildSuperRole());
        // 构建返回客户端内容
        return this.loginSuccess(user, roles);
    }

    /**
     * 二次登录验证
     * @return 二次验证标识
     */
    private RequestResult buildReAuth(UserDO user) {
        // 生成验证码
        if (this.reAuthAgingMap == null) {
            this.reAuthAgingMap = new AgingMap<>();
        }
        this.sendReAuthCaptcha(user.getId());
        // 返回前端结果
        return RequestResult.success()
                .appendData("key", user.getId())
                .appendData("reAuth", true);
    }

    /**
     * 校验注册角色是否合法
     * @param roleId 目标角色ID
     * @return 如果角色ID合法则返回当前角色ID，否则返回默认角色ID
     */
    private Integer verifyRegRoleId(Integer roleId) {
        if (roleId == null) {
            return loginProperties.getDefaultRoleId();
        } else if (!loginProperties.getAllowRole().contains(roleId)) {
            throw new LoginException("非法角色");
        }
        return roleId;
    }

    /**
     * 校验验证码
     * @param key 验证码的key
     * @param captcha 用户输入的验证码值
     */
    private void verifyRegCaptcha(String key, String captcha) {
        if (!agingMap.exist(key, captcha)) {
            agingMap.remove(key);
            throw new LoginException("验证码错误或已超时");
        }
    }

    /**
     * 通过注册信息创建UserDO
     * @param regVO 注册信息
     * @return UserDO
     */
    private UserDO getUserDOByRegVO(RegVO regVO) {
        UserDO user = new UserDO();
        user.setLoginName(regVO.getEmail() != null ? regVO.getEmail() : regVO.getPhone()!=null ? regVO.getPhone() : regVO.getLoginName());
        user.setEmail(regVO.getEmail());
        user.setPhone(regVO.getPhone());
        user.setPassWord(regVO.getPassWord());
        user.setNickName(regVO.getNickName());
        user.setInviterId(userService.getUserIdByInviteCode(regVO.getInvitationCode()));
        return user;
    }

    /**
     * 判断该账号是否注册
     *
     * @return true 已注册，false 未注册
     */
    private UserDO verifyReg(RegVO regVO) {
        //判断注册类型
        switch (regVO.getRegModel()) {
            case EMAIL:
                return userService.existUserByEmail(regVO.getEmail());
            case PHONE:
                return userService.existUserByPhone(regVO.getPhone());
            default:
                return null;
        }
    }

    /**
     * 判断用户登录是否有效
     * @param loginVO 登录参数
     * @return 用户信息
     */
    private UserDO verifyLogin(LoginVO loginVO,HttpServletRequest request) {
        UserDO userDO = null;
        switch (loginVO.getLoginModel()) {
            case LOGIN_NAME:
                userDO = userService.getUserByNameAndPwd(loginVO.getLoginName(), loginVO.getPassword());
                if(userDO == null){this.loginFail(loginVO,request,"账号或密码错误");}
                break;
            case EMAIL:
                userDO = userService.getUserByEmailAndPwd(loginVO.getLoginName(), loginVO.getPassword());
                if(userDO == null){this.loginFail(loginVO,request,"邮箱或密码错误");}
                break;
            case PHONE:
                userDO = userService.getUserByPhoneAndPwd(loginVO.getLoginName(), loginVO.getPassword());
                if(userDO == null){this.loginFail(loginVO,request,"手机号或密码错误");}
                break;
            case ID:
                userDO = userService.getUserByIDAndPwd(loginVO.getLoginName(), loginVO.getPassword());
                if(userDO == null){this.loginFail(loginVO,request,"唯一标识或密码错误");}
                break;
            default:
                throw new LoginException("登录方式异常");
        }
        return userDO;
    }

    /**
     * 登录失败
     *
     * @param loginVO
     * @param request
     */
    private void loginFail(LoginVO loginVO, HttpServletRequest request, String failTips) {
        this.saveLog(loginVO, LoginStateConst.LOGIN_FAIL, request);
        authManager.record(request); // 记录登录失败
        throw new LoginException(failTips);
    }
}
